/*
 * DataPackage.java
 *
 * Created on June 11, 2007, 11:40 AM
 *
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fpdev.core;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.fpdev.core.basenet.BLink;
import org.fpdev.core.basenet.BNode;
import org.fpdev.core.basenet.BaseNetFeatureIO;
import org.fpdev.core.data.LegacyDB;
import org.fpdev.core.data.SpatialDataConnection;
import org.fpdev.core.data.LegacyDerbyDB;
import org.fpdev.core.data.LegacyMysqlDB;
import org.fpdev.core.data.PostgisConnection;
import org.fpdev.apps.rtemaster.GUIOutputAcceptor;
import org.fpdev.core.data.CoreDB;
import org.fpdev.core.data.DummyCoreDB;
import org.fpdev.core.data.MysqlCoreDB;
import org.fpdev.core.data.ShapefileDataConnection;
import org.fpdev.util.FPUtil;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.FeatureSource;
import org.geotools.data.FeatureStore;
import org.geotools.data.postgis.PostgisDataStoreFactory;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.geotools.referencing.CRS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * A DataPackage contains all of the spatial information for a geographically
 * defined area. This includes the main base geospatial network (accessed via
 * the SpatialDataConnection class) and the Scenario hierarchy.
 *
 * <p>Currently, the software only works with one DataPackage at a time. This
 * will need to change, however, if multi-package features are to be supported
 * in the future (for instance, inter-regional trip planning).
 *
 * <p>TODO: Transit-specific items, such as the collections of routes, stations,
 * etc., are still stored in the FPEngine class. These should migrate to the
 * DataPackage class eventually.
 *
 *
 * @author demory
 */
public class DataPackage {

  private FPEngine engine_;

  private String name_; // the name of this DataPackage
  private Properties props_; // DP-specific text-based properties
  private Scenarios scenarios_; // the hierarchy of Scenarios

  // Core database -- handles basic non-spatial data storage needs (query
  // logging, archiving, etc.).
  private CoreDB coreDB_;

  // legacy db -- continued support for non-standard 5P spatial db's from
  // earlier versions. To be phased out over time.
  private LegacyDB legDB_; 
  
  private BaseNetFeatureIO featureIO_;
  private SpatialDataConnection spatialData_;


  /** Creates a new instance of DataPackage */
  public DataPackage(String name, FPEngine engine) {
    engine_ = engine;
    featureIO_ = new BaseNetFeatureIO(engine);

    name_ = name;
    
    props_ = new Properties();
    scenarios_ = new Scenarios();
    readXMLFile();
    
    //FPUtil.readPropertiesFile(props_, engine_.getProperty("5pHome") + "data" + File.separator + name_ + File.separator + "properties.xml");

    if(props_.getProperty("dataSource").equals("postgis"))
      spatialData_ = new PostgisConnection(engine, this);
    else if(props_.getProperty("dataSource").equals("shp"))
      spatialData_ = new ShapefileDataConnection(engine, this);
    else
      spatialData_ = new DummyDataConnection();

    if(props_.getProperty("dbUsername").length()==0)
      coreDB_ = new DummyCoreDB();
    else
      coreDB_ = new MysqlCoreDB(engine_, this, props_.getProperty("dbUsername"), props_.getProperty("dbPassword"));

    // legacy spatial db support:
    legDB_ = null;
    switch (LegacyDB.getRDBMSCode(props_.getProperty("dataSource"))) {
      case LegacyDB.DB_DERBY:
        legDB_ = new LegacyDerbyDB(engine_, this);
        break;
      case LegacyDB.DB_MYSQL:
        System.out.println("sending user: "+props_.getProperty("dbUsername"));
        legDB_ = new LegacyMysqlDB(engine_, this, props_.getProperty("dbUsername"), props_.getProperty("dbPassword"));
        break;
    }

  }

  public String getName() {
    return name_;
  }

  public SpatialDataConnection getSpatialDataConn() {
    return spatialData_;
  }

  public BaseNetFeatureIO getFeatureIO() {
    return featureIO_;
  }

  public Scenarios getScenarios() {
    return scenarios_;
  }

  public CoreDB getCoreDB() {
    return coreDB_;
  }

  public LegacyDB getLegacyDB() {
    return legDB_;
  }

  public String getPath() {
    return engine_.getProperty("5pHome") + "data" + File.separator + name_ + File.separator;
  }

  public void readXMLFile() {
    readXMLFile(true);
  }
  
  public void readXMLFile(boolean readScenarios) {
    try {
      String filename = getPath() + "datapackage.xml";
      DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document doc = docBuilder.parse(filename);
      NodeList docNodes = doc.getChildNodes();
      Node thisNode = docNodes.item(0);
      if (docNodes.getLength() != 1 && !thisNode.getNodeName().equals("datapackage")) {
        System.out.println("Not a valid datapackage properties file");
        return;
      }

      NodeList nodes = thisNode.getChildNodes();

      for (int n = 0; n < nodes.getLength(); n++) {
        Node node = nodes.item(n);
        
        // check for properties block
        if (node.getNodeName().compareTo("properties") == 0) {
          System.out.println("found properties block");
          NodeList propNodes = node.getChildNodes();

          for (int i = 0; i < propNodes.getLength(); i++) {
            Node propNode = propNodes.item(i);
            if (propNode.getNodeName().compareTo("property") == 0) {
              String name = propNode.getAttributes().getNamedItem("name").getNodeValue();
              System.out.println("prop name=" + name + " val=" + propNode.getTextContent());
              props_.setProperty(name, propNode.getTextContent());
            }
          }
        }
        
        if(!readScenarios) return;
        
        // check for scenarios block
        if (node.getNodeName().compareTo("basescenario") == 0) {
          readChildScenarios(node, scenarios_.getBase());
        }        
      }
      
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void close() {
     if(props_.getProperty("dataSource").equals("postgis"))
      spatialData_.close();
  }
  
  private void readChildScenarios(Node xmlNode, Scenario parent) {
    NodeList childNodes = xmlNode.getChildNodes();

    for (int i = 0; i < childNodes.getLength(); i++) {
      Node childNode = childNodes.item(i);
      if (childNode.getNodeName().compareTo("scenario") == 0) {
        short id = new Short(childNode.getAttributes().getNamedItem("id").getNodeValue()).shortValue();              
        String name = childNode.getAttributes().getNamedItem("name").getNodeValue();
        Scenario childScen = new Scenario(id, name, parent); 
        scenarios_.add(childScen);      

        // check for children of this node
        if(childNode.hasChildNodes()) this.readChildScenarios(childNode, childScen); 
      }
    }
  }

  public String getProperty(String name) {
    //System.out.println("gp: "+name);
    return props_.getProperty(name);
  }

  public void writeArchive(File archive) {
    String dpPath = getPath();

    // specify files to include in the ZIP file
    //String[] filenames = new String[] {dpPath+"routes.xml", dpPath+"stations.xml", dpPath+"streetalias.xml"};
    String[] filenames = new String[]{"datapackage.xml", "routes.xml", "stations.xml", "streetalias.xml"};

    // Create a buffer for reading the files
    byte[] buf = new byte[1024];

    try {
      // Create the ZIP file
      ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(archive));
      zos.setLevel(Deflater.DEFAULT_COMPRESSION);

      // Compress the files
      for (int i = 0; i < filenames.length; i++) {
        FileInputStream in = new FileInputStream(dpPath + filenames[i]);

        // Add ZIP entry to output stream.
        zos.putNextEntry(new ZipEntry(filenames[i]));

        // Transfer bytes from the file to the ZIP file
        int len;
        while ((len = in.read(buf)) > 0) {
          zos.write(buf, 0, len);
        }

        // Complete the entry
        zos.closeEntry();
        in.close();
        System.out.println("wrote " + filenames[i]);
      }

      FPUtil.zipDir(dpPath + "route", zos, dpPath.length());

      legDB_.dumpToZip(zos);

      zos.close();

    } catch (IOException ex) {
      ex.printStackTrace();
    }

  }

  public void deleteFromDisk() {
    if (name_.length() > 0) {
      FPUtil.deleteDirectory(engine_.getProperty("5pHome") + "data" + File.separator + name_);
      legDB_.delete();
      legDB_ = null;
    }
  }

  public boolean restoreFromArchive(File archive) {
    deleteFromDisk();

    // create data directory
    String dir = engine_.getProperty("5pHome") + "data" + File.separator + name_;
    new File(dir).mkdir();

    // load from zip file 
    Enumeration entries;
    ZipFile zipFile;

    try {
      zipFile = new ZipFile(archive);

      entries = zipFile.entries();

      while (entries.hasMoreElements()) {
        ZipEntry entry = (ZipEntry) entries.nextElement();
        String eName = entry.getName();
        if (eName.startsWith("mysql") || eName.startsWith("derby")) {
          continue;
        }

        if (entry.isDirectory()) {
          // Assume directories are stored parents first then children.
          System.err.println("Extracting directory: " + entry.getName());
          (new File(dir + File.separator + eName)).mkdir();
          continue;
        }

        //System.err.println("Extracting file: " + eName);
        eName = FPUtil.fixFilename(eName);
        (new File(dir + File.separator + eName).getParentFile()).mkdirs();
        FPUtil.copyInputStream(zipFile.getInputStream(entry),
                new BufferedOutputStream(new FileOutputStream(dir + File.separator + eName)));

      }

      zipFile.close();
    } catch (IOException ex) {
      ex.printStackTrace();
      return false;
    }

    // reload properties
    //props_ = new Properties();
    //FPUtil.readPropertiesFile(props_, dir + File.separator + "properties.xml");
    this.readXMLFile(false);
    
    try {

      // restore db

      legDB_ = null;
      zipFile = new ZipFile(archive);
      switch (LegacyDB.getRDBMSCode(props_.getProperty("rdbms"))) {
        case LegacyDB.DB_DERBY:
          legDB_ = new LegacyDerbyDB(engine_, this, zipFile);
          break;
        case LegacyDB.DB_MYSQL:
          legDB_ = new LegacyMysqlDB(engine_, this,  engine_.getProperty("dbUsername"), engine_.getProperty("dbPassword"), zipFile);
          break;
      }
      if (legDB_ != null) {
        return true;
      } else {
        System.out.println("Invalid rdbms: " + props_.getProperty("rdbms"));
      }

    } catch (Exception ex) {
      ex.printStackTrace();
    }
    return false;

  }
  
  public static void initDP(String name, String desc, double initX, double initY, Properties sysProps) {
    // create new directories
    String dataDir = sysProps.getProperty(SysProps.FP_HOME)+"data"+File.separator+name+File.separator;
    new File(dataDir).mkdir();
    //new File(sysProps.getProperty(SysProps.FP_HOME)+"data"+File.separator+name).mkdir();
    
 
    try {
      String filename = dataDir+"datapackage.xml";
      FileWriter writer = new FileWriter(filename);

      writer.write("<?xml version=\"1.0\"?>\n");
      writer.write("<datapackage name=\""+name+"\" desc=\""+desc+"\">\n");	
      writer.write("  <properties>\n");
      writer.write("    <property name=\"dataSource\"></property>\n");
      writer.write("    <property name=\"logQueries\">false</property>\n");
      writer.write("    <property name=\"loadNetworkCache\">false</property>\n");
      writer.write("    <property name=\"loadTransitCache\">false</property>\n");
      writer.write("    <property name=\"dbUsername\"></property>\n");
      writer.write("    <property name=\"dbPassword\"></property>\n");
      writer.write("    <property name=\"initX\">"+initX+"</property>\n");
      writer.write("    <property name=\"initY\">"+initY+"</property>\n");
      writer.write("    <property name=\"initRes\">50</property>\n");
      writer.write("  </properties>\n");
      writer.write("\n");
      writer.write("  <basescenario>\n");
      writer.write("  </basescenario>\n");
      writer.write("\n");
      writer.write("</datapackage>\n");
      
      writer.close();

    } catch(Exception ex) {
      ex.printStackTrace();
    } 
    
    createDataFile(dataDir+"routes.xml", new String[] { "providers", "routes" });
    createDataFile(dataDir+"stations.xml", new String[] { "stations" });
    //createDataFile(dataDir+"streetalias.xml", new String[] { "streetaliases" });
      
  }
  
  private static void createDataFile(String filename, String[] elements) {
    try {
      FileWriter writer = new FileWriter(filename);

      writer.write("<?xml version=\"1.0\"?>\n");
      writer.write("<fivepoints>\n");
      for (int i = 0; i < elements.length; i++) {
        String element = elements[i];
        writer.write("<"+element+">\n");;

        writer.write("</"+element+">\n");        
      }
      writer.write("</fivepoints>\n");
      
      writer.close();

      // write the routes file
      
    } catch(Exception ex) {
      ex.printStackTrace();
    }    
    
  }

  //** NETWORK INPUT METHODS **//

  public void readEntireNetwork() {
    System.out.println("Reading all nodes..");
    spatialData_.allNodes();
    System.out.println("Reading all links..");
    spatialData_.allLinks();
  }

  public void readNetworkFromSHP() {
    String home = engine_.getProperty("5pHome"), fs = File.separator;
    String dpName = engine_.getDataPackage().getName();

    // READ NODES
    try {
      // get feature results
      File file = new File(home + "data" + fs + dpName + fs + "shp" + fs + "nodes.shp");
      if(!file.exists()) {
        System.out.println("Fatal error: nodes shapefile does not exist");
        System.exit(1);
      }

      URL shapeURL = file.toURI().toURL();
      ShapefileDataStore store = new ShapefileDataStore(shapeURL);
      readNetworkNodes(store.getFeatureSource(store.getTypeNames()[0]));

    } catch (Exception e) {
      System.out.println("Error reading nodes shapefile:");
      e.printStackTrace();
    }


    // READ LINKS
    try {
      File file = new File(home + "data" + fs + dpName + fs + "shp" + fs + "links.shp");
      if(!file.exists()) {
        System.out.println("Fatal error: links shapefile does not exist");
        System.exit(1);
      }

      URL shapeURL = file.toURI().toURL();
      ShapefileDataStore store = new ShapefileDataStore(shapeURL);
      readNetworkLinks(store.getFeatureSource(store.getTypeNames()[0]));
  
    } catch (Exception e) {
      System.out.println("Error reading nodes shapefile:");
      e.printStackTrace();
      System.exit(0);
    }

  }

  public void readNetworkFromPostgis() {

    try {
      readNetworkNodes(((PostgisConnection) spatialData_).getNodeSource());
      readNetworkLinks(((PostgisConnection) spatialData_).getLinkSource());
    } catch(Exception e) {
      e.printStackTrace();
    }

  }

  private void readNetworkNodes(FeatureSource source) {
    try {
      // feature type name is defaulted to the name of shapefile (without extension)
      //String name = inStore.getTypeNames()[0];
      //FeatureSource source = inStore.getFeatureSource(name);
      FeatureCollection fColl = source.getFeatures();

      System.out.println("Feature collection type: " + fColl.getSchema().getName());
      System.out.println("Num features: " + fColl.size());

      //AttributeType[] attrs = store.getSchema().getAttributeTypes();

      FeatureIterator fIter = fColl.features();
      int c = 0;
      while (fIter.hasNext()) {
        c++;
        if (c % 10000 == 0) {
          System.out.println("loaded " + c + " features");
        }

        SimpleFeature f = (SimpleFeature) fIter.next();

        BNode node = featureIO_.readNode(f);
        if(node != null) engine_.getBaseNet().addNode(node);
        
      }
      System.out.println("loaded " + c + " features total");
    } catch(Exception ex) {
      ex.printStackTrace();
    }
  }

  private void readNetworkLinks(FeatureSource source) {
    try {
      //String name = inStore.getTypeNames()[0];
      //FeatureSource source = inStore.getFeatureSource(name);
      FeatureCollection fColl = source.getFeatures();

      System.out.println("Feature collection type: " + fColl.getSchema().getName());
      System.out.println("Num features: " + fColl.size());

      //AttributeType[] attrs = store.getSchema().getAttributeTypes();

      FeatureIterator fIter = fColl.features();
      int c = 0;
      while (fIter.hasNext()) {
        c++;
        if (c % 10000 == 0) {
          System.out.println("loaded " + c + " features");
        }

        SimpleFeature f = (SimpleFeature) fIter.next();

        BLink link = featureIO_.readLink(f);
        if(link != null) engine_.getBaseNet().addLink(link);
        
      }
      System.out.println("loaded " + c + " features total");
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }


  //** NETWORK OUTPUT METHODS **//

  public void writeNetworkToSHP(GUIOutputAcceptor gui) {

    String home = engine_.getProperty("5pHome"), fs = File.separator;
    String dpName = engine_.getDataPackage().getName();
    
    // initialize the CRS
    CoordinateReferenceSystem crs = null;

    try {
      crs = CRS.decode(getProperty("crsCode"));
    } catch (Exception ex) {
      Logger.getLogger(DataPackage.class.getName()).log(Level.SEVERE, null, ex);
    }

    /*String sphmerc = "PROJCS[\"Google Mercator\", GEOGCS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",SPHEROID[\"WGS 84\", 6378137.0, 298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\", 0.0, AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\", 0.017453292519943295],AXIS[\"Geodetic latitude\", NORTH],AXIS[\"Geodetic longitude\", EAST],AUTHORITY[\"EPSG\",\"4326\"]],PROJECTION[\"Mercator_1SP\"],PARAMETER[\"semi_minor\", 6378137.0],PARAMETER[\"latitude_of_origin\", 0.0],PARAMETER[\"central_meridian\", 0.0],PARAMETER[\"scale_factor\", 1.0],PARAMETER[\"false_easting\", 0.0],PARAMETER[\"false_northing\", 0.0],UNIT[\"m\", 1.0],AXIS[\"Easting\", EAST],AXIS[\"Northing\", NORTH],AUTHORITY[\"EPSG\",\"900913\"]]";
    CoordinateReferenceSystem crs = null;
    try {
      crs = CRS.parseWKT(sphmerc);
    } catch (FactoryException ex) {
      Logger.getLogger(DataPackage.class.getName()).log(Level.SEVERE, null, ex);
    }*/


    File shpDir = new File(home + "data" + fs + dpName + fs + "shp");
    if(!shpDir.exists()) shpDir.mkdir();

    // WRITE NODES FILE
    gui.statusText("Writing nodes...");
    try {
      File file = new File(home + "data" + fs + dpName + fs + "shp" + fs + "nodes.shp");
      if(!file.exists()) file.createNewFile();

      // create the datastore
      ShapefileDataStore outStore = new ShapefileDataStore(file.toURI().toURL());

      //System.out.println("crs: "+crs.getName());
      outStore.forceSchemaCRS(crs);
      SimpleFeatureType featureType = featureIO_.getNodeFeatureType();
      outStore.createSchema(featureType);

      featureIO_.writeNodes(engine_.getBaseNet().getNodes(), (FeatureStore) outStore.getFeatureSource(featureType.getName()));
      //writeNetworkNodes(outStore, gui);
      gui.msg("Nodes shapefile written");

    } catch(Exception e) {
      e.printStackTrace();
    }

    gui.statusText("Writing links...");
    try {
      File file = new File(home + "data" + fs + dpName + fs + "shp" + fs + "links.shp");
      if(!file.exists()) file.createNewFile();

      // create the datastore
      ShapefileDataStore outStore = new ShapefileDataStore(file.toURI().toURL());

      // initialize the CRS
      outStore.forceSchemaCRS(crs);
      SimpleFeatureType featureType = featureIO_.getLinkFeatureType();
      outStore.createSchema(featureType);

      //writeNetworkLinks(outStore, gui);
      featureIO_.writeLinks(engine_.getBaseNet().getLinks(), (FeatureStore) outStore.getFeatureSource(featureType.getName()));
      gui.msg("Links shapefile written");

    } catch(Exception e) {
      e.printStackTrace();
    }
    gui.statusText("");
  }

  public void writeNetworkToPostgis(GUIOutputAcceptor gui) {
    
    try {

      // create the datastore
      java.util.Map params = new java.util.HashMap();
      params.put(PostgisDataStoreFactory.DBTYPE.key, "postgis");
      params.put(PostgisDataStoreFactory.HOST.key, "localhost");
      params.put(PostgisDataStoreFactory.PORT.key, new Integer(5432));
      params.put(PostgisDataStoreFactory.SCHEMA.key, "public");

      String dbName = "fp_"+engine_.getDataPackage().getName();
      String username = getProperty("dbUsername");
      String password = getProperty("dbPassword");
      /*System.out.println("dbname="+dbName);
      System.out.println("username="+username);
      System.out.println("password="+password);*/
      params.put(PostgisDataStoreFactory.DATABASE.key, dbName);
      params.put(PostgisDataStoreFactory.USER.key, username);
      params.put(PostgisDataStoreFactory.PASSWD.key, password);

      DataStore outStore = DataStoreFinder.getDataStore(params);
      System.out.println("datastore="+outStore);

      gui.statusText("Writing nodes...");
      SimpleFeatureType nodeFT = featureIO_.getNodeFeatureTypePostgis();
      outStore.createSchema(nodeFT);
      featureIO_.writeNodes(engine_.getBaseNet().getNodes(), (FeatureStore) outStore.getFeatureSource(nodeFT.getName()));
      gui.msg("Nodes table written");

      gui.statusText("Writing links...");
      SimpleFeatureType linkFT = featureIO_.getLinkFeatureType();
      outStore.createSchema(linkFT);
      featureIO_.writeLinks(engine_.getBaseNet().getLinks(), (FeatureStore) outStore.getFeatureSource(linkFT.getName()));
      gui.msg("Links table written");

      gui.statusText("");

      outStore.dispose();

    } catch(Exception e) {
      e.printStackTrace();
    }
    
  }


  public class DummyDataConnection implements SpatialDataConnection {

    public void close() { }

    public void createNode(BNode node) { }

    public void deleteNode(BNode node) { }

    public void createLink(BLink link) { }

    public void deleteLink(BLink link) { }

    public void modifyNode(BNode node, NodeAttr[] attrs) { }

    public void modifyLink(BLink link, LinkAttr[] attrs) { }

    public void modifyNodes(Collection<BNode> nodes, NodeAttr[] attrs) { }

    public void modifyLinks(Collection<BLink> links, LinkAttr[] attrs) { }

    public BNode nodeFromID(int id) { return null; }

    public Collection<BNode> allNodes() { return null; }

    public Collection<BNode> nearbyNodes(double x, double y, double r) {
      return null;
    }

    public Collection<BNode> nodesInRange(double x1, double y1, double x2, double y2) {
      return null;
    }


    public Collection<BLink> allLinks() {
      return null;
    }

    public Collection<BLink> linksInRange(double x1, double y1, double x2, double y2) {
      return null;
    }

    public Collection<BLink> linksInSet(Collection<Integer> ids) {
      throw new UnsupportedOperationException("Not supported yet.");
    }

    public Collection<BNode> nodesInSet(Collection<Integer> ids) {
      throw new UnsupportedOperationException("Not supported yet.");
    }

  }
}
